package jmine.tec.web.servlet.security;

import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.security.auth.Subject;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jmine.tec.cache.ExpiringReference;
import jmine.tec.web.servlet.cipher.SecureCommunicationFilter;
import bancosys.tec.persist.controller.TransactionalController;
import bancosys.tec.security.authorization.GroupPermission;
import bancosys.tec.security.authorization.Permission;
import bancosys.tec.security.impl.SecurityPrincipal;
import bancosys.tec.security.impl.dao.GroupDAO;
import bancosys.tec.security.impl.domain.Credential;
import bancosys.tec.security.impl.domain.Group;
import bancosys.tec.security.impl.web.WebSecurityContext;
import bancosys.tec.security.impl.web.servlet.SecurityServlet;

/**
 * Servlet de login especializado para o canal de comunicacao segura
 * 
 * @author takeshi
 */
public class SecureCommunicationLoginServlet extends SecurityServlet {

    private static final long DEFAULT_CACHE_DURATION = 6000L;

    private TransactionalController transactionalController;

    private long cacheDuration = DEFAULT_CACHE_DURATION;

    private ExpiringReference<Map<String, Collection<? extends Permission>>> cache = null;

    /**
     * {@inheritDoc}
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if (SecureCommunicationFilter.isSecureChannel()) {
            String[] groups = this.getGroupNames(req);
            String userName = this.getUserName(req);
            if (userName == null) {
                resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
            if (groups == null || groups.length == 0) {
                resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Grupo inexistente no request");
                return;
            }
            Subject subject = this.createSubject(groups, userName);
            this.getSecurityManager().storeSubject(new WebSecurityContext(req, resp), subject);
        } else {
            super.doPost(req, resp);
        }
    }

    /**
     * Devolve o nome de usuario
     * 
     * @param req {@link HttpServletResponse}
     * @return String
     */
    protected String getUserName(HttpServletRequest req) {
        return req.getParameter("username");
    }

    /**
     * Devolve os grupos
     * 
     * @param req {@link HttpServletRequest}
     * @return String[]
     */
    protected String[] getGroupNames(HttpServletRequest req) {
        return req.getParameterValues("groups");
    }

    /**
     * Cria um subject
     * 
     * @param groups String[]
     * @param userName String
     * @return {@link Subject}
     */
    protected Subject createSubject(String[] groups, String userName) {
        Set<Principal> principals = new HashSet<Principal>();
        principals.add(new SecurityPrincipal(userName));
        Set<Permission> pubCredentials = new HashSet<Permission>();
        pubCredentials.addAll(this.getPublicPermissions(groups));
        return new Subject(true, principals, pubCredentials, Collections.emptySet());
    }

    /**
     * Extrai as permissoes publicas dos grupos passados
     * 
     * @param groups os nomes dos grupos
     * @return {@link Collection} de {@link Permission}
     */
    protected Collection<Permission> getPublicPermissions(String[] groups) {
        Collection<Permission> permissions = new ArrayList<Permission>();
        this.getTransactionalController().startTransaction();
        try {
            for (String groupName : groups) {
                permissions.add(new GroupPermission(groupName));
                if (this.getPermissions().containsKey(groupName)) {
                    Collection<? extends Permission> groupPermissions = this.getPermissions().get(groupName);
                    if (groupPermissions != null && !groupPermissions.isEmpty()) {
                        permissions.addAll(groupPermissions);
                    }
                }
            }
        } finally {
            this.getTransactionalController().commit();
        }
        return permissions;
    }

    /**
     * Obtém o mapa de permissões, construindo-o, caso necessário.
     * 
     * @return mapa de permissões
     */
    private Map<String, Collection<? extends Permission>> getPermissions() {
        Map<String, Collection<? extends Permission>> permissions = this.cache == null ? null : this.cache.getReferentOrNull();
        if (permissions == null) {
            permissions = new HashMap<String, Collection<? extends Permission>>();
            for (Group group : this.getTransactionalController().getDAOFactory().getDAOByClass(GroupDAO.class).findAll()) {
                Set<Credential> credentials = group.getCredentials();
                if (credentials != null && !credentials.isEmpty()) {
                    permissions.put(group.getName(), this.toPermissions(credentials));
                }
            }
            this.cache = new ExpiringReference<Map<String, Collection<? extends Permission>>>(permissions, this.cacheDuration);
        }
        return permissions;
    }

    /**
     * Transforma uma colecao de credenciais em uma colecao de permissoes
     * 
     * @param credentials {@link Collection} de {@link Credential}
     * @return {@link Collection} de {@link Permission}
     */
    protected Collection<? extends Permission> toPermissions(Collection<? extends Credential> credentials) {
        List<Permission> list = new ArrayList<Permission>(credentials.size());
        for (Credential credential : credentials) {
            list.add(credential.createPermission());
        }
        return list;
    }

    /**
     * @return the transactionalController
     */
    public TransactionalController getTransactionalController() {
        return this.transactionalController;
    }

    /**
     * @param transactionalController the transactionalController to set
     */
    public void setTransactionalController(TransactionalController transactionalController) {
        this.transactionalController = transactionalController;
    }

    /**
     * @return duração do cache, em milisegundos.
     */
    public long getCacheDuration() {
        return this.cacheDuration;
    }

    /**
     * Define a duração do cache, em milisegundos
     * 
     * @param cacheDuration cacheDuration
     */
    public void setCacheDuration(long cacheDuration) {
        this.cacheDuration = cacheDuration;
    }

}
